<?php
/**
 * Чтение записей всех типов: алфавитный список, общая лента, форум
 *
 * @package default
 * @author Alexander Sibiryakov
 **/
class AggregatedModel_Feed_ReaderItems implements AggregatedModel_Feed_FeedInterface
{
	// предопределенные параметры
	protected $page_citems = 5;
	protected $struct_type = "native"; // 'letters', 'years'

	protected $tag;
	protected $tdis;
	protected $page_number;
	protected $filter_by_day;
	protected $user_oid;
	protected $filter_by_doctype;
	protected $orderby;
	protected $current_tdi_oid; // установить если надо ограничить выборку только в рамках текущего раздела
	protected $community_oid; // устанавливать всегда
	protected $count; // сколько записей вернуть, если установлено то вернет с певой странице указанное кол-во записей
	protected $item_oid; // установить, если нужно получить один конкретный итем
	protected $no_pager; // установить, если не нужна листалка вообще
	protected $join_community; // установить, если нужно к каждой записи вытащить данные для сообщества
	protected $tdisInCommunity = array();

	public $rh;

	public function setModelParams($key, $value) {
		$this->$key = $value;
	}

	public function pushTdiPerCommunity( $community_oid, $tdi_oid ) {
		$this->tdisInCommunity[$community_oid][] = $tdi_oid;
	}

	public function getTdiPerCommunity() {
		return $this->tdisInCommunity;
	}

	public function doStuff() {
		// тут собственно формируется логика того каким условиям должны удовлетворять итемы
		// и сохраняется в виде части запроса where
		$q = $this->prepare_where_query(
			$this->tag,
			$this->tdis,
			$this->community_oid,
			$this->filter_by_day,
			$this->user_oid,
			$this->filter_by_doctype,
			$this->author_oid,
			$this->tdisInCommunity
		);

		// скажет нам сколько страниц, по части запроса where
		$this->pages_count = $this->prepare_pages_count( $q );

		// HACK : 2008-01-17 : lucky
		//
//		$this->q = $q;

		$this->pager = $this->prepare_pager_structure( $this->page_number, $this->pages_count );

		$this->items = $this->fetch_posts( $q, $this->page_number, $this->struct_type );

	}

	public function getItems() {
		return $this->items;
	}

	public function getPager() {
		return array(
			"prev"=>$this->pager['prev'],
			"next"=>$this->pager['next'],
			"overall_items_count"=>$this->items_count,
			"items_generated"=>count($this->items),
			"items_per_page"=>$this->page_citems,
			"pages_count"=>$this->pages_count
			);
	}

	public function clean() {

	}

	public function getTdiForFeedPerCommunity () {

		$res = array();
		$q = $this->prepare_where_query(
			$this->tag,
			$this->tdis_enum,
			$this->community_oid,
			$this->filter_by_day,
			$this->user_oid,
			$this->filter_by_doctype
		);

		$prefix = $this->rh->ctx->store->db->db_prefix;
		$sql = "SELECT allfeed.tdi_oid AS oid, allfeed.community_oid,
				COUNT(*) AS `count` FROM {$prefix}objects
				INNER JOIN {$prefix}class_allfeed allfeed ON allfeed.oid = {$prefix}objects.oid
				INNER JOIN t_acl_tree tat ON tat.id_object = {$prefix}objects.oid AND tat.is_deleted = 0
		".$q." GROUP BY allfeed.tdi_oid, allfeed.community_oid
				ORDER BY allfeed.community_oid, allfeed.tdi_oid";

		$total_items = 0;
		if ($rs = $this->rh->ctx->store->query($sql)) {
			while ($row = $rs->next()) {
				$count = intval($row['count']);
				$res[] = array('section'=>$row['oid'], 'count' => $count,
				'community_oid' => $row['community_oid'] );
				$total_items += $count;
			}
		}

		return $res;
	}

	/**
	 * Возвращает разделы для ленты с кол-вом записей в них.
	 * Использовалось для фильтрации по разделам при выводе лент.
	 * @return array
	 **/
	public function getTdiForFeed () {

		$res = array();
		$q = $this->prepare_where_query(
			$this->tag,
			$this->tdis_enum,
			$this->community_oid,
			$this->filter_by_day,
			$this->user_oid,
			$this->filter_by_doctype
		);

		$prefix = $this->rh->ctx->store->db->db_prefix;
		$sql = "SELECT allfeed.tdi_oid AS oid, COUNT(*) AS `count` FROM {$prefix}objects
				INNER JOIN {$prefix}class_allfeed allfeed ON allfeed.oid = {$prefix}objects.oid
				INNER JOIN t_acl_tree tat ON tat.id_object = {$prefix}objects.oid AND tat.is_deleted = 0
		".$q." GROUP BY allfeed.tdi_oid";

		$total_items = 0;
		if ($rs = $this->rh->ctx->store->query($sql)) {
			while ($row = $rs->next()) {
				$oid = $row['oid'];

				$count = intval($row['count']);
				$res[$oid] = array('section'=>$oid, 'count' => $count);
				$total_items += $count;
			}
		}

		return $res;
	} // end of method getTdiForFeed

	/**
	 * Возвращает типы документов в ленте с кол-вом записей в них.
	 * Использовалось для фильтрации по разделам при выводе лент.
	 * @return array
	 **/
	public function getDocTypesForFeed () {

		$res = array();

		$q = $this->prepare_where_query(
			$this->tag,
			$this->tdis_enum,
			$this->community_oid,
			$this->filter_by_day,
			$this->user_oid /*,
			$this->filter_by_doctype*/
		);

		$prefix = $this->rh->ctx->store->db->db_prefix;
		$sql = "SELECT {$prefix}objects.type AS type, COUNT(*) AS `count` FROM {$prefix}objects
				INNER JOIN {$prefix}class_allfeed allfeed ON allfeed.oid = {$prefix}objects.oid
				INNER JOIN t_acl_tree tat ON tat.id_object = {$prefix}objects.oid AND tat.is_deleted = 0
		".$q." GROUP BY {$prefix}objects.type";

		$total_items = 0;
		$doctypes = $this->rh->ctx->doctypes;

		if ($rs = $this->rh->ctx->store->query($sql)) {

			while ($row = $rs->next()) {
				$ftype = $row['type'];
				$type = substr($ftype, 1); // fdoc -> doc

				$count = intval($row['count']);
				$type_struct = isset($doctypes[$type]) ? $doctypes[$type] : NULL;
				$res[$type] = array('type'=>$type_struct, 'count' => $count);

				$total_items += $count;
			}

		}

		if ($res) {
			$total = array(
				'section' => array(
					'oid' => 0,
					'name' => 'Total',
				),
			);
			$res = array(
				'all' => array(
					'type' => array(
						'typename' => 'Все',
					),
					'is_total' => true,
					'count' => $total_items,
				),
			) + $res;
		}

		return $res;
	} // end of method getTdiForFeed

	protected function prepare_where_query( $tag, $tdis, $community_oid, $filter_by_day=NULL, $user_oid=NULL, $filter_by_doctype=NULL, $author_oid = NULL, $tdi_per_comm = NULL ) {
		// подготовит для вывода в шаблоны структуру с разделами, а также структуру для запроса к хранилищу
		$db = $this->rh->ctx->store->db;
		$prefix = $db->db_prefix;

		$sql_parts = array();
		if ($community_oid)
			$sql_parts[] = ' allfeed.community_oid = '.$community_oid;

		if (isset($this->current_tdi_oid) && empty($tdis)) {
			$sql_parts[] = ' allfeed.tdi_oid = '.$this->current_tdi_oid;
		}

		// --- sections --- {{{
		if (!empty($tdis)) {
			$sql_parts[] = ' allfeed.tdi_oid IN ('.implode(',', $tdis).')';
		}

		// --- filter_by_doctype --- {{{
		if (!empty($filter_by_doctype)) {
			$ftypes = array();
			if (!is_array($filter_by_doctype)) $filter_by_doctype = array($filter_by_doctype);
			foreach ($filter_by_doctype as $doctype) {
				// если выбрали "Все" выбираем
				if ($doctype === 'all') { $ftypes = array(); break; }
				$ftypes[] = $db->quote("f{$doctype}");
			}
			if ($ftypes) {
				$sql_parts[] = " {$prefix}objects.type IN (".implode(',', $ftypes).')';
			}
			unset($ftypes);
		}
		// --- filter_by_doctype --- }}}

		// --- tags --- {{{
		if (isset($tag)) {
			$sql_parts[] = " allfeed.oid IN (SELECT subject_oid AS oid FROM store_tag_subject WHERE tag_oid in (select oid from store_class_tag where lower(name) like '{$tag->data['name']}')) ";
		}
		// --- tags --- }}}

		// --- filter_by_day --- {{{
		if (isset($filter_by_day)) {
			$sql_parts[] = " DATE(store_objects.ctime)=".$db->quote($filter_by_day);
		}
		// --- filter_by_day --- }}}

		// --- user --- {{{
		if (isset($user_oid)) {
			$sql_parts[] = ' allfeed.posted_by_id = '.$user_oid;
		}
		// --- user --- }}}

		// --- author --- {{{
		if (isset($author_oid)) {
			$sql_parts[] = ' item_author.author_oid = '.$author_oid;
		}
		// --- author --- }}}

		if (isset($this->item_oid)) $sql_parts[] = " allfeed.oid=".$this->item_oid;

		if ( !empty($tdi_per_comm) ) {
			$parts = array();
			foreach ($tdi_per_comm as $key=>$val) {
				$parts[] = "(allfeed.community_oid = {$key} and allfeed.tdi_oid in " . ' (' . join(',', $val) . '))';
			}

			$sql_parts[] = '(' . join( ' or ', $parts ) . ')';
		}

		$sql_parts[] = "{$prefix}objects.is_deleted = 0";
		$sql_parts[] = "allfeed.community_oid > 0";

		$sql = ' where '.implode(" and ", $sql_parts);

		if (isset($author_oid)) {
			$sql = " INNER JOIN {$prefix}item_author item_author ON allfeed.oid = item_author.item_oid " . $sql;

		}

		return $sql;
	}

	protected function prepare_items_count( $q ) {
		// вернет количество итемов в результатах запроса
		$res = NULL;
		$prefix = $this->rh->ctx->db->db_prefix;
		$sql = "SELECT count({$prefix}objects.oid) as count FROM {$prefix}objects
				INNER JOIN {$prefix}class_allfeed allfeed ON allfeed.oid = {$prefix}objects.oid
		".$q;
		$rs_count = $this->rh->ctx->store->findSQL( $sql );
 		if ($rs_count) {
 			$res = $rs_count->next()->data['count'];
 		} else {
			$res = FALSE;
 		}

		return $res;
	}

	protected function prepare_pages_count( $q ) {
		// вернет количество страниц необходимое для того чтобы вместить все данные
		$this->items_count = $this->prepare_items_count($q);
		return ceil($this->items_count / $this->page_citems);
	}

	protected function prepare_pager_structure( $page_number, $pages_count ) {
		// подготовит структуру для вывода постраничной листалки
		$pager = array();
		if (($page_number-1) > 0) $pager['prev'] = $page_number - 1;
		if (($page_number+1) <= $pages_count) $pager['next'] = $page_number + 1;

		return $pager;
	}

	/**
	 * Возвращает структуру с постингами. Исходную структуру создает метод _native, остальные ее меняют. Я считаю что это также
	 * нужно выносить в отдельные объекты.
	 *
	 * @return void
	 * @author Alexander Sibiryakov
	 **/
	protected function fetch_posts ( $q, $page_number, $struct_type ) {
		switch ($struct_type) {
			case 'native':
			return $this->fetch_posts_native($q, $page_number);

			case 'letters':
			return $this->fetch_posts_letters($q, $page_number);

			case 'years':
			return $this->fetch_posts_years($q, $page_number);

			default:
			return $this->fetch_posts_native($q, $page_number);
		}
	}

	protected function fetch_posts_native( $q, $page_number ) {
		$prefix = $this->rh->ctx->db->db_prefix;

		$user_oids = array();
    if ( isset($this->rh->ctx->mm->aa) && !empty($this->rh->ctx->mm->aa) ) {
      $aa = $this->rh->ctx->mm->aa;
      if ( isset($aa->acl_data['user_id']) && !empty($aa->acl_data['user_id']) ) {
        $user_oids[] = $aa->acl_data['user_id'];
      }
      if ( isset($aa->acl_data['role']['oid']) && !empty($aa->acl_data['role']['oid']) ) {
        $user_oids[] = $aa->acl_data['role']['oid'];
      }
    }
    $user_oids_string = join(',', $user_oids);

		$sql = "SELECT
				{$prefix}objects.*,
				user.status,
				user.avatar,
				user.fullname,
				user.oid AS user_oid,
				allfeed.edited_user_oid,
				allfeed.edited_dtime,
				(select count(*) FROM t_acl_tree as node
				left join t_acl_tree as parent on parent.lft <= node.lft and parent.rgt >= node.rgt
				left outer join t_acl_list on parent.id_object = t_acl_list.id_acl_tree
				where node.id_object = {$prefix}objects.oid and t_acl_list.id_subject in ({$user_oids_string}) and
				t_acl_list.`action` in ('item_read') and t_acl_list.opt = 2) as disabled,
				(SELECT COUNT(*)
				FROM `{$prefix}class_comment`
				left join `{$prefix}objects` on `{$prefix}objects`.oid = `{$prefix}class_comment`.oid
				INNER JOIN t_acl_tree tat ON tat.id_object = `{$prefix}class_comment`.oid AND tat.is_deleted = 0
				WHERE `{$prefix}objects`.is_deleted = 0 and
				`{$prefix}class_comment`.`item_oid` = allfeed.oid) AS `comments_count`
				FROM {$prefix}objects
				INNER JOIN {$prefix}class_allfeed allfeed ON allfeed.oid = {$prefix}objects.oid
				LEFT OUTER JOIN {$prefix}class_user user ON user.oid = allfeed.posted_by_id
		".$q." ".$parameters . " having `disabled` = 0 ";

		// извлекает посты по where-части запроса и номеру страницы

		$sql .= 'order by ctime desc ';

		if ( !isset($this->no_pager) ) {
			$sql .= ' limit '.($page_number-1) * $this->page_citems.','.$this->page_citems;
		}

		$rs = $this->rh->ctx->store->findSQL( $sql );

		if (!$rs) { return false; }

		$items = array();
		$m_community = $this->rh->ctx->mm->getModel('Community', 'm');
		$cache_communities = array();
		while ($obj = $rs->next()) {
			$row = $rs->rs->current();
			$data = array_merge($obj->data, $row);

			$data = $this->batchItemProcess($data);

			$item = array();

			// [FIXIT : 2008-06-02 : lucky]
			//
			// Саша, для чего это?  У разных типов документов наборы полей могут
			// сильно отличаться друг от друга. Думаю, в этом месте не следует делать
			// априорных предположений, об информаци, содержащейся в исходной
			// структуре данных. Ведь эта модель должна принимать любые типы.
			//
			// Если, для ленты, какая-то информация нужна в специфичном формате, это
			// следует задокументировать и реализовать преобразование в соответствующих
			// *ItemProcess*'орах.
			//
			// лучше $items['post'] = $data;
			$item['post']['tdi_oid'] = $data['tdi_oid'];
			$item['post']['title'] = $data['title'];
			$item['post']['announce'] = $data['announce'];
			$item['post']['body'] = $data['body'];
			$item['post']['tags'] = $data['tags'];
//			$item['post']['author_url']
			$item['post']['issue_oid'] = $data['issue_oid'];
			$item['post']['posted_by_id'] = $data['posted_by_id'];
			$item['post']['community_oid'] = $data['community_oid'];
			$item['post']['date_till'] = $data['date_till'];
			$item['post']['oid'] = $data['oid'];
			$item['post']['type'] = $data['type'];
			$item['post']['is_deleted'] = $data['is_deleted'];
			$item['post']['ops'] = $data['ops'];

			$item["post"]["oid"] = $data['oid'];
			$item['post']['ctime'] = RL_CyrDate::newFromStr("%Y-%m-%d %H:%i:%s", $data['ctime']);
			$item['post']['commentscount'] = $data['comments_count'];
			$item['post']['is_deleted'] = $data['is_deleted'];
			$item['post']['question'] = $data['question'];
			$item['post']['answers'] = $data['answers'];
			$item['post']['max_voters'] = $data['max_voters'];
			$item['post']['overall_voters'] = $data['overall_voters'];
			$item['post']['image_oid'] = $data['image_oid'];
			$item['post']['file_oid'] = $data['file_oid'];
			$item['post']['url'] = $data['url'];
			$item['post']['description'] = $data['description'];
			// issue fields
			$item['post']['publish_date'] = $data['publish_date'];
			$item['post']['issue_descr'] = $data['issue_descr'];
			$item['post']['issue_number'] = $data['issue_number'];
			$item['post']['issue_name'] = $data['issue_name'];
			// review fields
			$item['post']['isdn'] = $data['isdn'];
			$item['post']['subject_url'] = $data['subject_url'];
			$item['post']['subject_title'] = $data['subject_title'];
			$item['post']['subject_authors'] = $data['subject_authors'];
			$item['post']['subject_description'] = $data['subject_description'];

			//allfeed
			if ( isset($data['edited_user_oid']) && !empty($data['edited_user_oid']) ) {
				$item['post']['edited_user_oid'] = $data['edited_user_oid'];
				$item['post']['edited_dtime'] = RL_CyrDate::newFromStr("%Y-%m-%d %H:%i:%s", $data['edited_dtime']);
			}

			// quest
			if (isset($data['anonce']))      $item['post']['announce'] = $data['anonce'];
			if (isset($data['descript']))    $item['post']['description'] = $data['descript'];
			if (isset($data['is_finished'])) $item['post']['is_finished'] = $data['is_finished'];
			if (isset($data['post_anonce'])) $item['post']['post_announce'] = $data['post_anonce'];

			// videotube
			if (isset($data['tube_html']))      $item['post']['tube_html'] = $data['tube_html'];
			if (isset($data['tube_mce_html']))    $item['post']['tube_mce_html'] = $data['tube_mce_html'];

			// authors
			$item['post']['authors'] = $data['authors']; // authors plain string
			$item['Authors'] = $data['Authors']; // array of parsed authors

			$item['poster'] = array(
					"status" => $data['status'],
					  "name" => $data['fullname'],
					"avatar" => $data['avatar'],
					   "oid" => $data['user_oid']
							);
			$item['issue'] = $data['issue'];
			$item['vote_closed'] = $data['vote_closed'];
			$item['platonicaid_author'] = $data['platonicaid_author'];
			$item['image'] = $data['image'];

			$item['file'] = $data['file'];
			$item['tags'] = $data['tags'];

			// -- community -- {{{
			$community_oid = $data['community_oid'];
			if (!isset($cache_communities[$community_oid])) {
				$community = $m_community->getCommunity($data['community_oid']);
				$cache_communities[$community_oid]        = $community->data;
				$cache_communities[$community_oid]['oid'] = $community->id;
				$cache_communities[$community_oid]['url'] = $community->data['community_url'];
			}
			$item['community'] = $cache_communities[$community_oid];
			// -- community -- }}}

			$item['doctype'] = substr($data['type'], 1);

			$items[] = $item;
			unset($item);
		}
		return $items;
	}

	protected function batchItemProcess($data) {
		$processors = array(
			"IssueField",
			"VoteType",
			"IdsrvUrlField",
			"ImageType",
			"FileType",
			"TagsField",
			"AuthorsField",
			);
		if (isset($this->join_community)) {
			$processors[] = "CommunityField";
		}

		$this->rh->useClass("AggregatedModel_ItemProcessor_Interface");
		$this->rh->useClass("AggregatedModel_ItemProcessor_Common");
		foreach ($processors AS $pname) {
			$cname = "AggregatedModel_ItemProcessor_".$pname;
			$this->rh->useClass($cname);
			$ig = new $cname($this->rh);
			$data = $ig->processItem($data);
			unset($ig);
		}

		return $data;
	}

	protected function fetch_posts_letters( $q, $page_number ) {
		$items = $this->fetch_posts_native($q, $page_number);

		if ( empty($items) ) {
			return array();
		}

		// var_dump(uniord('А')); // 1040
		// var_dump(uniord('Я')); // 1071
/*
		var_dump(uniord('а')); // 1072
		var_dump(uniord('я')); // 1103

		var_dump(uniord('A')); // 65
		var_dump(uniord('Z')); // 90

		var_dump(uniord('a')); // 97
		var_dump(uniord('z')); // 122

		var_dump(uniord('0')); // 97
		var_dump(uniord('9')); // 122
*/
		// создаем буквенные группы, такой группой может быть к пачка записей начинающихся на русские буквы или
		// промежуток 0-9 цифр или латинских букв A-Z

		for ($i=1040;$i<=1071;$i++) {
			$uni = $this->unichr($i);
			$letters[ $uni ]['letter_group'] = $uni;
		}

		$letters[ '0-9' ]['letter_group'] = '0-9';
		$letters[ 'A-Z' ]['letter_group'] = 'A-Z';

		foreach($items as $item)
		{
			// назначение символьной группе
			$first_letter = mb_substr($item['post']['title'],0,1);
			$first_letter = mb_convert_case($first_letter, MB_CASE_UPPER);
			$ord = $this->uniord( $first_letter );

			if ($ord >=65 && $ord <= 90) { // латиниские символы
				$letters[ 'A-Z' ]['items'][] = $item;
			} elseif ($ord >=48 && $ord <= 57) { // циферные символы
				$letters[ '0-9' ]['items'][] = $item;
			} else {
				$letters[ $first_letter ]['items'][] = $item;
			}
		}
		return $letters;
	}

	protected function fetch_posts_years( $q, $page_number ) {
		$items = $this->fetch_posts_native($q, $page_number);

		foreach($items as $item)
		{

			if (isset($item['post']['publish_date'])) {
				$year = RL_CyrDate::newFromStr( "%Y-%m-%d", $item['post']['publish_date'] )->year();
				$item['post']['publish_date'] = RL_CyrDate::newFromStr("%Y-%m-%d", $item['post']['publish_date']);
			} elseif (isset($item['post']['date'])) {
				$year = RL_CyrDate::newFromStr( "%Y-%m-%d %H:%i:%s", $item['post']['date'] )->year();
			} else {
				$year = $item['post']['ctime']->year();
			}

			$years[ $year ]['items'][] = $item;
			$years[ $year ]['year_group'] = $year;
		}
		return $years;
	}

	// эти функции написал Miguel Perez
	// http://ru2.php.net/manual/ru/function.ord.php#77905
	protected function uniord($c) {
	    $h = ord($c{0});
	    if ($h <= 0x7F) {
	        return $h;
	    } else if ($h < 0xC2) {
	        return false;
	    } else if ($h <= 0xDF) {
	        return ($h & 0x1F) << 6 | (ord($c{1}) & 0x3F);
	    } else if ($h <= 0xEF) {
	        return ($h & 0x0F) << 12 | (ord($c{1}) & 0x3F) << 6
	                                 | (ord($c{2}) & 0x3F);
	    } else if ($h <= 0xF4) {
	        return ($h & 0x0F) << 18 | (ord($c{1}) & 0x3F) << 12
	                                 | (ord($c{2}) & 0x3F) << 6
	                                 | (ord($c{3}) & 0x3F);
	    } else {
	        return false;
	    }
	}

	// http://ru2.php.net/manual/ru/function.chr.php#77911
	protected function unichr($c) {
	    if ($c <= 0x7F) {
	        return chr($c);
	    } else if ($c <= 0x7FF) {
	        return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
	    } else if ($c <= 0xFFFF) {
	        return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
	                                    . chr(0x80 | $c & 0x3F);
	    } else if ($c <= 0x10FFFF) {
	        return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
	                                    . chr(0x80 | $c >> 6 & 0x3F)
	                                    . chr(0x80 | $c & 0x3F);
	    } else {
	        return false;
	    }
	}

} // END class
